﻿using System.Transactions;
using FluentAssertions;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using O1.Kernel.Primitives;
using O1.Kernel.Storage;
using O1.Kernel.Storage.Transactions;

namespace Tests.O1.Kernel
{
    [TestClass]
    public class MultiVersionBinaryStorageTests
    {
        [TestMethod, TestCategory("Unit Test")]
        public void MultiVersionBinaryStorageInitializesIntoProcessingState()
        {
            using (var subject = new MultiVersionBinaryStorage(1000, new StorageSequence()))
            {
                subject.State.Should().Be(WriterQueueState.Active);
            }
        }

        [TestMethod, TestCategory("Unit Test")]
        public void MultiVersionBinaryStorageDisposesIntoIdleState()
        {
            using (var subject = new MultiVersionBinaryStorage(1000, new StorageSequence()))
            {
                subject.Dispose();
                while (subject.State != WriterQueueState.Idle) ;
                subject.State.Should().Be(WriterQueueState.Idle);
            }
        }

        [TestMethod, TestCategory("Unit Test")]
        public void MultiVersionBinaryStorageReturnsTrueForAddWhenActive()
        {
            var highestCommitted = ((long)3).SetCommitted();
            var newCell = BuildTestCell(7, long.MaxValue);

            var sequence = new StorageSequence();
            using (var subject = new MultiVersionBinaryStorage(150, sequence))
            {
                var token = new ConcurrencyToken(7, highestCommitted);
                subject.Write(sequence.NextId(), newCell, token).Should().BeTrue();
            }
        }

        [TestMethod, TestCategory("Unit Test")]
        public void MultiVersionBinaryStorageReturnsFalseForAddWhenIdle()
        {
            var highestCommitted = ((long)3).SetCommitted();
            var newCell = BuildTestCell(7, long.MaxValue);

            var sequence = new StorageSequence();
            using (var subject = new MultiVersionBinaryStorage(150, sequence))
            {
                var token = new ConcurrencyToken(7, highestCommitted);
                subject.Dispose();
                while (subject.State != WriterQueueState.Idle) ;
                subject.Write(sequence.NextId(), newCell, token).Should().BeFalse();
            }
        }

        [TestMethod, TestCategory("Unit Test")]
        public void MultiVersionBinaryStorageReturnsTrueForModifyWhenActive()
        {
            var highestCommitted = ((long)3).SetCommitted();
            var headers = new Tx[] 
                {
                    new Tx(((long)2).SetCommitted(), long.MaxValue),
                    new Tx(((long)1).SetCommitted(), long.MaxValue),
                };

            var storage = new MultiVersionBinaryArray(300);
            storage[123] = BuildTestCells(headers);
            var newCell = BuildTestCell(7, long.MaxValue);

            var sequence = new StorageSequence();
            using (var subject = new MultiVersionBinaryStorage(storage, sequence, 124, new long[0]))
            {
                var token = new ConcurrencyToken(7, highestCommitted);
                subject.Write(123, newCell, token).Should().BeTrue();
            }
        }

        [TestMethod, TestCategory("Unit Test")]
        public void MultiVersionBinaryStorageReturnsFalseForModifyWhenIdle()
        {
            var highestCommitted = ((long)3).SetCommitted();
            var headers = new Tx[] 
                {
                    new Tx(((long)2).SetCommitted(), long.MaxValue),
                    new Tx(((long)1).SetCommitted(), long.MaxValue),
                };

            var storage = new MultiVersionBinaryArray(300);
            storage[123] = BuildTestCells(headers);
            var newCell = BuildTestCell(7, long.MaxValue);

            var sequence = new StorageSequence();
            using (var subject = new MultiVersionBinaryStorage(storage, sequence, 124, new long[0]))
            {
                var token = new ConcurrencyToken(7, highestCommitted);
                subject.Dispose();
                while (subject.State != WriterQueueState.Idle) ;
                subject.Write(123, newCell, token).Should().BeFalse();
            }
        }
        [TestMethod, TestCategory("Unit Test")]
        public void MultiVersionBinaryStorageModifyShowsCommittedReadWhenDifferentScope()
        {
            var highestCommitted = ((long)3).SetCommitted();
            var headers = new Tx[] 
                {
                    new Tx((long)6, long.MaxValue),
                    new Tx((long)5, long.MaxValue),
                    new Tx((long)4, long.MaxValue),
                    new Tx(((long)3).SetCommitted(), long.MaxValue),
                    new Tx(((long)2).SetCommitted(), long.MaxValue),
                    new Tx(((long)1).SetCommitted(), long.MaxValue),
                };

            var storage = new MultiVersionBinaryArray(300);
            storage[123] = BuildTestCells(headers);

            var sequence = new StorageSequence();
            using (var subject = new MultiVersionBinaryStorage(storage, sequence, 124, new long[0]))
            {
                var update = BuildTestCell(7, long.MaxValue);
                var token = new ConcurrencyToken(7, highestCommitted);
                var readerToken = new ConcurrencyToken(highestCommitted, highestCommitted);
                subject.Write(123, update, token);

                // TODO: Using same token, should this be scope visible?
                var data = subject.Read(123, readerToken);
                var readTx = Tx.Read(data, 0);
                readTx.AddedTxId.Should().Be(highestCommitted);
            }
        }

        [TestMethod, TestCategory("Unit Test")]
        public void MultiVersionBinaryStorageModifyShowsUncommittedReadWhenInSameScopeAndReadCommitted()
        {
            var highestCommitted = ((long)3).SetCommitted();
            var headers = new Tx[] 
                {
                    new Tx((long)6, long.MaxValue),
                    new Tx((long)5, long.MaxValue),
                    new Tx((long)4, long.MaxValue),
                    new Tx(((long)3).SetCommitted(), long.MaxValue),
                    new Tx(((long)2).SetCommitted(), long.MaxValue),
                    new Tx(((long)1).SetCommitted(), long.MaxValue),
                };

            var storage = new MultiVersionBinaryArray(300);
            storage[123] = BuildTestCells(headers);

            var sequence = new StorageSequence();
            using (var subject = new MultiVersionBinaryStorage(storage, sequence, 124, new long[0]))
            {
                var update = BuildTestCell(7, long.MaxValue));
                var token = new ConcurrencyToken(7, highestCommitted);
                var written = false;
                token.ValueWrittenCallback += (o, id) => { if (id == 123) { written = true; } };
                subject.Write(123, update, token);
                // Using same token, this is scope visible...a race exists between writer and reader. The caller has the option to synchronize.
                while (!written) ;
                var data = subject.Read(123, token);
                var readTx = Tx.Read(data, 0);
                readTx.AddedTxId.Should().Be(7);
            }
        }

        [TestMethod, TestCategory("Unit Test")]
        public void MultiVersionBinaryStorageModifyShowsUncommittedReadWhenReadUncommitted()
        {
            var highestCommitted = ((long)3).SetCommitted();
            var headers = new Tx[] 
                {
                    new Tx((long)6, long.MaxValue),
                    new Tx((long)5, long.MaxValue),
                    new Tx((long)4, long.MaxValue),
                    new Tx(((long)3).SetCommitted(), long.MaxValue),
                    new Tx(((long)2).SetCommitted(), long.MaxValue),
                    new Tx(((long)1).SetCommitted(), long.MaxValue),
                };

            var storage = new MultiVersionBinaryArray(300);
            storage[123] = BuildTestCells(headers);

            var sequence = new StorageSequence();
            using (var subject = new MultiVersionBinaryStorage(storage, sequence, 124, new long[0]))
            {
                var update = BuildTestCell(7, long.MaxValue);
                var token = new ConcurrencyToken(7, highestCommitted);
                var written = false;
                token.ValueWrittenCallback += (o, id) => { if (id == 123) { written = true; } };
                subject.Write(123, update, token);
                
                // TODO: When reading inside the same TX, the modification is not visible because of the 
                // async processing. How will a 'same scope' TX be blocked? This scenario needs a synchronization mechanism.                
                while (!written) ;
                MultiVersion<byte[]> version;
                var exists = subject.(123, token);
                var readTx = (data, 0);
                readTx.AddedTxId.Should().Be(7);
            }
        }

        private static MultiContainer<byte[]> BuildTestCells(Tx[] txs, long lastTxId = 0)
        {
            // write a cell header, and n* edges
            var result = new MultiVersion<byte[]>[txs.Length];
            for (int i = 0; i < result.Length; ++i)
            {
                result[i] = BuildTestCell(txs[i].AddedTx, txs[i].DeletedTx);
            }

            var container = new MultiContainer<byte[]>();
            container.Swap(result, lastTxId);
            return container;
        }

        private static MultiVersion<byte[]> BuildTestCell(long start, long end)
        {
            var data = new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
            return new MultiVersion<byte[]>(start, end, data);
        }

        private static byte[] BuildTestCell()
        {
            return new byte[] { 1, 2, 3, 4, 5, 6, 7, 8, 9, 0 };
        }

        private struct Tx
        {
            public long AddedTx;

            public long DeletedTx;

            public Tx(long addedTx, long deletedTx)
                : this()
            {
                this.AddedTx = addedTx;
                this.DeletedTx = deletedTx;
            }
        }
    }
}